Skip to content

Conversation

@marcin-serwin
Copy link
Contributor

@marcin-serwin marcin-serwin commented Nov 8, 2025

According to the docs, SDL_PixelFormat is a read-only structure. Creating it manually leaves out some important fields like format and next pointer to be undefined.


EDIT Starbuck5:
This was needed for SDL2 compat support to pass certain tests, now it's unclear whether it's needed.

@marcin-serwin marcin-serwin requested a review from a team as a code owner November 8, 2025 18:49
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

📝 Walkthrough

Walkthrough

Replaces stack-allocated SDL_PixelFormat usage with dynamically allocated SDL_PixelFormat* via SDL_AllocFormat(SDL_MasksToPixelFormatEnum(...)) across surf_convert, surf_convert_alpha, and pgSurface_Blit in src_c/surface.c; applies palettes with SDL_SetPixelFormatPalette(...) for indexed formats and ensures SDL_FreeFormat(...) is called on all paths.

Changes

Cohort / File(s) Summary
Pixel format allocation & conversion updates
src_c/surface.c
Replace stack SDL_PixelFormat instances with SDL_AllocFormat(format_enum) in surf_convert and surf_convert_alpha; compute format_enum via SDL_MasksToPixelFormatEnum(...), use allocated format for SDL_ConvertSurface/PG_ConvertSurface, apply palette via SDL_SetPixelFormatPalette(format, ...) for indexed formats, and free with SDL_FreeFormat(format) on all paths.
Blit path temporary format handling
src_c/surface.c
In pgSurface_Blit, replace local newfmt with SDL_AllocFormat(...); pass it to PG_ConvertSurface/SDL_BlitSurface, adjust palette handling to the allocated format, and ensure SDL_FreeFormat(newfmt) on success and error paths.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Blit/Convert caller
  participant Src as Source surface
  participant MaskEnum as SDL_MasksToPixelFormatEnum
  participant Alloc as SDL_AllocFormat
  participant Pal as SDL_SetPixelFormatPalette
  participant Conv as SDL_ConvertSurface / PG_ConvertSurface
  participant Free as SDL_FreeFormat
  participant Dest as Destination surface

  Caller->>MaskEnum: compute format_enum from masks/bpp
  MaskEnum-->>Caller: format_enum
  Caller->>Alloc: SDL_AllocFormat(format_enum)
  Alloc-->>Caller: format*
  alt indexed format
    Caller->>Pal: SDL_SetPixelFormatPalette(format*, palette)
  end
  Caller->>Conv: convert source using format* -> converted_src
  Conv-->>Caller: converted_src
  Caller->>Free: SDL_FreeFormat(format*)
  Caller->>Dest: SDL_BlitSurface(converted_src -> Dest)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify all early-return and error paths free allocated formats (SDL_FreeFormat).
  • Confirm palette application uses the allocated format and matches previous semantics.
  • Check for potential use-after-free when passing allocated format into conversion/blit functions.

Suggested reviewers

  • ankith26

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing manual SDL_PixelFormat creation with SDL_AllocFormat calls throughout the codebase.
Description check ✅ Passed The pull request description clearly explains the motivation: SDL_PixelFormat is read-only per SDL docs, and manual creation leaves important fields undefined. It references the need for SDL2 compatibility.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for contributing to pygame-ce! 🎉

@ankith26 ankith26 added this to the 2.5.7 milestone Nov 8, 2025
@marcin-serwin
Copy link
Contributor Author

I've run into the same issue in surf_convert. @ankith26 could you re-review?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src_c/surface.c (1)

4533-4545: Handle NULL from SDL_AllocFormat in blit path

The blit fallback has the same risk: if SDL_MasksToPixelFormatEnum yields something unsupported or we hit OOM, SDL_AllocFormat returns NULL. Passing that straight into PG_ConvertSurface will dereference it and crash. Guard the allocation, only blit once a converted surface exists, and propagate the error otherwise.

         else {
             SDL_PixelFormat *fmt = src->format;
-            SDL_PixelFormat *newfmt =
-                SDL_AllocFormat(SDL_MasksToPixelFormatEnum(
-                    fmt->BitsPerPixel, fmt->Rmask, fmt->Gmask, fmt->Bmask, 0));
-
-            src = PG_ConvertSurface(src, newfmt);
-
-            SDL_FreeFormat(newfmt);
-            if (src) {
+            Uint32 tmp_format_enum =
+                SDL_MasksToPixelFormatEnum(fmt->BitsPerPixel, fmt->Rmask,
+                                           fmt->Gmask, fmt->Bmask, 0);
+            SDL_PixelFormat *newfmt = SDL_AllocFormat(tmp_format_enum);
+            SDL_Surface *converted = NULL;
+
+            if (!newfmt) {
+                result = -1;
+            }
+            else {
+                converted = PG_ConvertSurface(src, newfmt);
+                SDL_FreeFormat(newfmt);
+            }
+
+            if (converted) {
+                src = converted;
 #if SDL_VERSION_ATLEAST(3, 0, 0)
                 result = SDL_BlitSurface(src, srcrect, dst, dstrect) ? 0 : -1;
 #else
                 result = SDL_BlitSurface(src, srcrect, dst, dstrect);
 #endif
                 SDL_FreeSurface(src);
             }
             else {
                 result = -1;
             }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3fbdc and 8b0eda1.

📒 Files selected for processing (1)
  • src_c/surface.c (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/surface.c
📚 Learning: 2025-09-01T20:22:31.010Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.

Applied to files:

  • src_c/surface.c
🧬 Code graph analysis (1)
src_c/surface.c (2)
src_c/base.c (1)
  • pg_UintFromObjIndex (591-604)
src_c/surflock.c (1)
  • pgSurface_Unprep (51-59)

@marcin-serwin marcin-serwin force-pushed the push-wmkrxtzwskyk branch 2 times, most recently from 3186ee9 to ffd60e2 Compare November 9, 2025 21:32
@aatle aatle added the Surface pygame.Surface label Nov 9, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src_c/surface.c (1)

1846-1846: Consider conditional palette allocation.

The palette is allocated unconditionally at line 1846 but only used for indexed formats (lines 1944-1956). This means non-indexed conversions allocate and free an unused palette. The SDL3 path (lines 1779-1790) demonstrates a more efficient approach by only allocating the palette when SDL_ISPIXELFORMAT_INDEXED(format_enum) is true.

Consider adopting the SDL3 pattern:

-            SDL_Palette *palette = SDL_AllocPalette(default_palette_size);
+            SDL_Palette *palette = NULL;
             Uint32 format_enum = 0;
 
             if (pg_IntFromObj(argobject, &bpp)) {
                 // ... compute format_enum ...
             }
             // ... other branches ...
+
             SDL_PixelFormat *format = SDL_AllocFormat(format_enum);
             if (!format) {
-                SDL_FreePalette(palette);
                 pgSurface_Unprep(self);
                 return RAISE(pgExc_SDLError, SDL_GetError());
             }
 
             if (SDL_ISPIXELFORMAT_INDEXED(format_enum)) {
                 if (SDL_ISPIXELFORMAT_INDEXED(PG_SURF_FORMATENUM(surf))) {
                     SDL_SetPixelFormatPalette(format, surf->format->palette);
                 }
                 else {
+                    palette = SDL_AllocPalette(default_palette_size);
+                    if (!palette) {
+                        SDL_FreeFormat(format);
+                        pgSurface_Unprep(self);
+                        return RAISE(pgExc_SDLError, SDL_GetError());
+                    }
                     SDL_SetPaletteColors(palette, default_palette_colors, 0,
                                          default_palette_size);
                     SDL_SetPixelFormatPalette(format, palette);
                 }
             }
             newsurf = PG_ConvertSurface(surf, format);
             SDL_SetSurfaceBlendMode(newsurf, SDL_BLENDMODE_NONE);
             SDL_FreeFormat(format);
-            SDL_FreePalette(palette);
+            if (SDL_ISPIXELFORMAT_INDEXED(format_enum) &&
+                !SDL_ISPIXELFORMAT_INDEXED(PG_SURF_FORMATENUM(surf))) {
+                SDL_FreePalette(palette);
+            }

Also applies to: 1960-1960

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffd60e2 and 035dd86.

📒 Files selected for processing (1)
  • src_c/surface.c (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/surface.c
📚 Learning: 2025-09-01T20:22:31.010Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.

Applied to files:

  • src_c/surface.c
🧬 Code graph analysis (1)
src_c/surface.c (2)
src_c/base.c (1)
  • pg_UintFromObjIndex (591-604)
src_c/surflock.c (1)
  • pgSurface_Unprep (51-59)

Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTm, thanks again! 🎉

According to the docs, `SDL_PixelFormat` is a read-only structure.
Creating it manually leaves out some important fields like `format` and
`next` pointer to be undefined.

Signed-off-by: Marcin Serwin <marcin@serwin.dev>
Copy link
Member

@Starbuck5 Starbuck5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I wanted to do stuff like this when porting SDL3 but I (probably with Ankith's encouragement), decided not to, in order to just leave the old code functioning how it is.

What is the justification for changing this code?

@marcin-serwin
Copy link
Contributor Author

What is the justification for changing this code?

Since libsdl-org/sdl2-compat#521 sdl2-compat relies on the format and next fields to be set correctly. The old code doesn't set them which causes some of the surface tests (e.g., GeneralSurfaceTests.test_convert_palettize, SurfaceSelfBlitTest.test_colorkey) to fail with the new 2.32.58 version.

@ankith26
Copy link
Member

It is interesting to note that the PR has been (partially) reverted as of libsdl-org/sdl2-compat#545 which was merged just a few days ago, which may change things.

However I think this PR is still worth merging because it is making the code more robust.

Copy link
Member

@Starbuck5 Starbuck5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

The second bit of code is ridiculous (not your fault), I hope the whole block can be deleted in SDL3.

I edited the description to add a tiny description about why this change is being made.

@Starbuck5 Starbuck5 merged commit 4d809d9 into pygame-community:main Nov 19, 2025
27 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gnu/Linux Surface pygame.Surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants